Add force_fused option to scaled_dot_product_attention - #4185
Conversation
|
Hi @hojin12312 — happy to report the M1 validation is done, and the results turned out more interesting than a simple thumbs-up. Thanks again for folding the safety and diagnostics work into your PR so cleanly; reviewing the capability-reason code while testing it on two machines was a pleasure, and I found nothing to object to — consider that portion reviewed and approved from my side. The setup: your branch at 1. The #3885 register cliff is still real — but it moved with the OS, and it's specialization-dependent.
(2-pass pipelines: 1024 in every variant; M4 Max: 1024 across the board.) Back in July, every d512 variant I probed reported 832. The current Metal compiler squeezes exactly one specialization under the dispatch size; everything else still caps below the kernel's hard-coded 1024-thread launch. I did not expect the ground to shift under this experiment in three weeks, but here we are. 2. The check behaves exactly as intended, end to end (
One reproduction note for anyone retracing this: at kL >= 1024 on 's'-class devices the dispatcher selects the 2-pass kernels (which fit everywhere), so the capped 1-pass is only reachable at shorter kL. My first run sat exactly on the wrong side of that boundary and sailed through — worth knowing before anyone concludes the hazard is gone. 3. Your The takeaway I'd offer for the PR description: the hazard these checks guard is not a static device property. It shifts with the Metal compiler version and with function-constant specialization — the same kernel is safe unmasked and unsafe masked, on the same chip, today. No support-matrix allowlist can track that, which makes the unconditional dispatch-time check exactly the right shape, and your full/NAX additions close the last unchecked dispatches. I'll close #4186 now — glad the two efforts converged, and looking forward to seeing this land. The d512 vector instantiations can then follow as the small opt-in PR discussed in #3658, with their safety story now validated on real hardware. |
c7709f1 to
7830a8a
Compare
bfbf563 to
f592a98
Compare
f592a98 to
7880475
Compare
Proposed changes
Add a
force_fusedoption tomx.fast.scaled_dot_product_attentionthatignores the automatic dispatch heuristics, uses a fused kernel, and raises
with the unsupported constraint when no fused kernel is available. This lets
inference runtimes — the only party that knows their resident weights, KV
cache size, and memory budget — decide when the fused path's bounded-memory
behavior is worth its throughput cost.
Also restores the fused full-attention support for
head_dim=192/256that wasproposed in #3293/#3660. Those kernels are now reachable only through
force_fused; the default dispatch does not route to them.Follow-up hardening incorporates the full-dispatch threadgroup safety check
and capability-diagnostic concerns raised by @apocryphx in #4186, while
retaining this PR's 192/256 kernels and C++ positional-call compatibility.
Background
#3658 tracks models with genuine
head_dim=192/256full-attention layers. Atlong context, the unfused path materializes a score
transient of
O(n_heads × qL × kL)per full-attention layer. The fusedfull-attention kernel tiles K/V so its transient stays bounded regardless of
kL. However, benchmark measurements on a genuinehead_dim=256workload(the Qwen 3.6 35B checkpoint measured in #3658) show the fused path trades
prompt throughput for that memory floor:
These stock
mlx-lmbenchmark arms were separated by other main-branchcommits, so they are supporting workload evidence rather than a strict
single-commit A/B. The optional 131K fused arm aborted with an
interactivity-impacting command-buffer error and is intentionally omitted.
A fixed dispatch threshold cannot pick correctly for all runtimes, so the
decision is exposed to the caller instead. This PR is the follow-up to the
maintainer request in #3658:
Supersedes the closed #3660.
Addresses #3658.
API
force_fused=False(default): preserves existing dispatch for previouslysupported shapes. The missing
head_dim=192vector kernel is added butreachable only through
force_fused.force_fused=True: bypass the dispatch heuristics and use a fused kernel.Raises
ValueErrorwith the unsupported constraint when no fused kernel isavailable, which covers:
(including unsupported
head_dims, e.g. 512);logsumexpoutput (the fused kernels do notemit one).
is_trainingon its own (when the VJP path already falls back) is allowed touse the fused forward kernel — the output is identical.
Kernel support
steel_attention): addhead_dim=192/256instantiations.Selected only when
force_fused=True. Extend the existing large-head-dimV-tile synchronization barriers from
BD == 128toBD >= 128, covering192/256 as well as 128. This synchronization gap was also independently
identified in giaki3003/mlx@1175b4d.
sdpa_vector): add the missinghead_dim=192(192,192)and aggregation instantiations. The new 192 vector kernel is opt-in via
force_fusedlike the new full-attention kernels; the existinghead_dim=256vector kernel keeps its automatic routing.
head_dim >= 192away from the NAX kernel family, which has no192/256 instantiations.
attention dispatches, avoiding silent zero output on register-limited GPUs.
cuDNN/vector kernels share the same opt-in semantics.
Validation
python/tests/test_fast_sdpa.py:force_fused=Truefull attention forhead_dim192/256 at short keylengths (where the default dispatch stays unfused) matches the reference;
force_fused=Truefull attention forhead_dim192/256 atkL=16385matches the reference, covering the Steel V-tile barrier path;
force_fused=Trueon an already-supportedhead_dim=128matches;force_fused=Trueon an unsupported shape (head_dim=512) raises;qL > kL, vector GQA limits,and CPU execution raise with the specific violated constraint;
head_dim192/256 full attention stays unfused andmatches the reference;
head_dim=192vector attention matches the reference.test_fast_sdpa.pyandtest_fast.pysuites continue to pass.Scope
head_dim=512, which is outside this PR's scope (see Metal SDPA: no fused path for head_dim=512 (vector or full kernel), and the fallback is silent #3885).head_dim=256layers there are bounded sliding-window layers.head_dim=256full attention and demonstrates the intended memory/throughputtradeoff. The optional 131K fused arm did not complete, so this PR does not
claim a valid 131K A/B result.
force_fused; a no-GPU buildraises rather than silently ignoring the flag. CUDA compilation remains a
GitHub CI validation boundary because the local validation host is macOS/Metal.
Checklist
pre-commit run --all-filesto format my code / installed pre-commit prior to committing changes